﻿namespace TuneMultiCheckBox.Logic.TuneMultiCheckBox
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Xml.Linq;
  using BusinessLogic;
  using Exception;
  using Microsoft.Xrm.Sdk;
  using Microsoft.Xrm.Sdk.Messages;
  using Microsoft.Xrm.Sdk.Metadata;
  using Microsoft.Xrm.Sdk.Metadata.Query;
  using Microsoft.Xrm.Sdk.Query;

  /// <summary>
  /// Class TuneMultiCheckBoxManager
  /// </summary>
  public class Manager : ManagerBase<Entity>
  {
    private const string SharedVariableKeyPrefix = "checkboxset_";
    private readonly char[] pairSeparator = new[] { '|' };
    private readonly Entity inputTargetEntity;
    private readonly string logicalName;

    /// <summary>
    /// Initializes a new instance of the <see cref="Manager"/> class. 
    /// </summary>
    /// <param name="localContext">
    /// The local context.
    /// </param>
    public Manager(PluginBase<Entity>.LocalPluginContext localContext) 
      : base(localContext)
    {
      var depth = localContext.PluginExecutionContext.Depth;
      if (depth > 1)
      {
        return;
      }

      LoadMetadataCache(localContext);

      this.inputTargetEntity = this.LocalContext.InputTargetEntity;
      this.logicalName = this.inputTargetEntity.LogicalName;
      if (ExcludedEntityList.Contains(this.logicalName))
      {
        return;
      }

      LoadConfig(localContext);
      
      this.ConfigurationEntity = Config.Entities.FirstOrDefault(r => r.LogicalName == this.logicalName);
      if (this.ConfigurationEntity == null)
      {
        return;
      }

      this.ValidateContext();
    }

    public static Configuration Config { get; set; }

    /// <summary>
    /// Gets or sets the list of entities that should not be processed anyway.
    /// </summary>
    /// <value>The excluded entity list.</value>
    public static HashSet<string> ExcludedEntityList { get; set; }

    public Configuration.Entity ConfigurationEntity { get; private set; }

    private bool ProcessRequired
    {
      get
      {
        return this.ConfigurationEntity != null;
      }
    }
    
    public static void UpdateConfigAsNeeded(PluginBase<Entity>.LocalPluginContext localContext)
    {
      var parameterXmlText = localContext.ParameterXml;
      if (parameterXmlText == null)
      {
        return;
      }

      var xml = XDocument.Parse(parameterXmlText, LoadOptions.None);
      if (xml.Root == null)
      {
        return;
      }

      Guid configWebresourceId;
      using (var orgContex = localContext.OrganizationContextAsSystemUser)
      {
        var resource = orgContex.WebResourceSet
          .Where(r => r.Name == Configuration.ConfigFileWebResourceName)
          .Select(r => new { r.WebResourceId })
          .FirstOrDefault();

        if (resource == null)
        {
          var message = string.Format("tuneMultiCheckBox configuration file / webresource ({0}) is not found.", Configuration.ConfigFileWebResourceName);
          throw Configuration.BuildConfigException(message);
        }

        configWebresourceId = resource.WebResourceId.Value;
      }

      var configPublished = 
        xml.Root.Descendants().Any(
          r => r.Name.LocalName == "webresource" 
            && r.Value.Trim().Equals(configWebresourceId.ToString("B").Trim(), StringComparison.InvariantCultureIgnoreCase));

      if (!configPublished)
      {
        return;
      }

      Config = null;
      LoadConfig(localContext);
    }

    public static void UpdateConfig(PluginBase<Entity>.LocalPluginContext localContext)
    {
      Config = null;
      LoadConfig(localContext);
    }

    public void PrepareRelationsUpdate()
    {
      if (!this.ProcessRequired)
      {
        return;
      }

      foreach (var item in this.ConfigurationEntity.CheckboxSets)
      {
        this.SaveFakeFieldValue(item);
      }
    }

    public void UpdateRelations()
    {
      if (!this.ProcessRequired)
      {
        return;
      }

      foreach (var item in this.ConfigurationEntity.CheckboxSets)
      {
        this.ProcessSetting(item);
      }
    }

    private static void LoadConfig(PluginBase<Entity>.LocalPluginContext localContext)
    {
      if (Config == null)
      {
        Config = new Configuration(localContext);
      }
    }

    private static void LoadMetadataCache(PluginBase<Entity>.LocalPluginContext localContext)
    {
      if (ExcludedEntityList != null)
      {
        return;
      }

      var filter = new MetadataFilterExpression(LogicalOperator.Or);
      filter.Conditions.Add(
        new MetadataConditionExpression("IsIntersect", MetadataConditionOperator.Equals, true));
      filter.Conditions.Add(
        new MetadataConditionExpression("IsCustomizable", MetadataConditionOperator.Equals, false));

      var query = new EntityQueryExpression
      {
        Criteria = filter
      };

      using (var orgContext = localContext.OrganizationContext)
      {
        var request = new RetrieveMetadataChangesRequest()
        {
          ClientVersionStamp = null,
          Query = query
        };

        var response = (RetrieveMetadataChangesResponse)orgContext.Execute(request);
        ExcludedEntityList = new HashSet<string>(response.EntityMetadata.Select(r => r.LogicalName));
      }
    }

    private void SaveFakeFieldValue(Configuration.CheckboxSet item)
    {
      var target = this.LocalContext.InputTargetEntity;
      var fakeFieldName = item.FakeSavingField;
      if (!target.Contains(fakeFieldName))
      {
        return;
      }

      var ids = target.GetAttributeValue<string>(fakeFieldName);
      var sharedVariableKey = SharedVariableKeyPrefix + fakeFieldName;
      this.LocalContext.AddSharedVariable(sharedVariableKey, ids);
      target[fakeFieldName] = null;
    }

    private void ProcessSetting(Configuration.CheckboxSet item)
    {
      var localContext = this.LocalContext;
      var fakeFieldName = item.FakeSavingField;
      var sharedVariableKey = SharedVariableKeyPrefix + fakeFieldName;
      var value = localContext.GetSharedVariable(sharedVariableKey, true);
      if (value == null)
      {
        return;
      }

      var ids = value.ToString();
      if (string.IsNullOrWhiteSpace(ids))
      {
        return;
      }

      var idsPair = ids.Split(this.pairSeparator);
      if (idsPair.Length != 2)
      {
        return;
      }

      var checkedIds = idsPair[0];
      var uncheckedIds = idsPair[1];
      var itemSeparator = new[] { ';' };
      var checkedIdStringList = checkedIds.Split(itemSeparator, StringSplitOptions.RemoveEmptyEntries);
      var checkedIdList = checkedIdStringList.Select(r => new Guid(r));
      var uncheckedIdStringList = uncheckedIds.Split(itemSeparator, StringSplitOptions.RemoveEmptyEntries);
      var uncheckedIdList = uncheckedIdStringList.Select(r => new Guid(r));
      var relationshipName = item.RelationshipName;

      var relationshipMetadata = this.GetRelationship(relationshipName);
      if (relationshipMetadata == null)
      {
        throw new InvalidPluginExecutionException(
          string.Format("Relationship {0} is not found.", relationshipName));
      }

      if (relationshipMetadata is ManyToManyRelationshipMetadata)
      {
        this.ProcessManyToManyRelationship((ManyToManyRelationshipMetadata)relationshipMetadata, checkedIdList, uncheckedIdList);
      }
      else
      {
        this.ProcessOneToManyRelationship((OneToManyRelationshipMetadata)relationshipMetadata, checkedIdList, uncheckedIdList);
      }
    }

    private void ProcessManyToManyRelationship(ManyToManyRelationshipMetadata relationshipMetadata, IEnumerable<Guid> checkedIdList, IEnumerable<Guid> uncheckedIdList)
    {
      var relationshipName = relationshipMetadata.SchemaName;
      if (relationshipMetadata.IsCustomRelationship == false)
      {
        throw new InvalidPluginExecutionException(string.Format("This out-of-box relationship ({0}) is not supported yet.", relationshipName));
      }

      var orgContex = this.LocalContext.OrganizationContext;
      var inputTarget = this.LocalContext.InputTargetEntity;
      var entity1LogicalName = inputTarget.LogicalName;
      string entity2LogicalName;

      string entity1IntersectAttribute;
      string entity2IntersectAttribute;
      if (relationshipMetadata.Entity1LogicalName == entity1LogicalName)
      {
        entity1IntersectAttribute = relationshipMetadata.Entity1IntersectAttribute;
        entity2LogicalName = relationshipMetadata.Entity2LogicalName;
        entity2IntersectAttribute = relationshipMetadata.Entity2IntersectAttribute;
      }
      else
      {
        entity1IntersectAttribute = relationshipMetadata.Entity2IntersectAttribute;
        entity2LogicalName = relationshipMetadata.Entity1LogicalName;
        entity2IntersectAttribute = relationshipMetadata.Entity1IntersectAttribute;
      }

      var intersectEntityName = relationshipMetadata.IntersectEntityName;
      var existingChecked = orgContex
        .CreateQuery(intersectEntityName)
        .Where(r => r[entity1IntersectAttribute] != null && ((Guid)r[entity1IntersectAttribute]) == inputTarget.Id)
        .Select(r => (Guid)r[entity2IntersectAttribute])
        .ToList();

      var itemsToAssociate = checkedIdList
        .Except(existingChecked)
        .Select(r => new EntityReference(entity2LogicalName, r))
        .ToList();

      this.Associate(relationshipName, itemsToAssociate);

      var itemsToDisassociate = uncheckedIdList
        .Intersect(existingChecked)
        .Select(r => new EntityReference(entity2LogicalName, r))
        .ToList();

      this.Disassociate(relationshipName, itemsToDisassociate);
    }

    private void ProcessOneToManyRelationship(OneToManyRelationshipMetadata relationshipMetadata, IEnumerable<Guid> checkedIdList, IEnumerable<Guid> uncheckedIdList)
    {
      throw new InvalidPluginExecutionException(
        string.Format("{0} relationship is 'one to many'. This type of relations is not implemented yet.", relationshipMetadata.SchemaName));
    }

    private void Associate(string relationshipName, IList<EntityReference> ids)
    {
      if (ids == null || ids.Count == 0)
      {
        return;
      }

      var inputTarget = this.LocalContext.InputTargetEntity;
      var request = new AssociateRequest
      {
        Target = inputTarget.ToEntityReference(),
        Relationship = new Relationship(relationshipName),
        RelatedEntities = new EntityReferenceCollection(ids)
      };

      var service = this.LocalContext.OrganizationService;
      service.Execute(request);
    }

    private void Disassociate(string relationshipName, IList<EntityReference> ids)
    {
      if (ids == null || ids.Count == 0)
      {
        return;
      }

      var inputTarget = this.LocalContext.InputTargetEntity;
      var request = new DisassociateRequest
      {
        Target = inputTarget.ToEntityReference(),
        Relationship = new Relationship(relationshipName),
        RelatedEntities = new EntityReferenceCollection(ids)
      };

      var service = this.LocalContext.OrganizationService;
      service.Execute(request);
    }

    private RelationshipMetadataBase GetRelationship(string name)
    {
      var service = this.LocalContext.OrganizationServiceAsSystemUser;
      var request = new RetrieveRelationshipRequest
      {
        Name = name
      };

      var response = (RetrieveRelationshipResponse)service.Execute(request);
      return response.RelationshipMetadata;
    }
  }
}
